//
//  LcLocationCenter.m
//  18Birdies
//
//  Created by Lc on 15/5/10.
//  Copyright (c) 2015年 Lc. All rights reserved.
//

#import "LcLocationCenter.h"
#import "LcLog.h"
#import "LcPreconditions.h"
#import "LcWeakReference.h"

@interface LcLocationCenter ()<CLLocationManagerDelegate>
@property(nonatomic, strong) CLLocationManager* locationManager;
@property(nonatomic, strong) NSMutableArray* weakDelegates;
@property(nonatomic, assign) BOOL locationStarting;
@property(nonatomic, assign) BOOL headingStarting;
@property(nonatomic, assign) BOOL usingHighAccuracyMode;

@property(nonatomic, assign) BOOL locationPaused;
@property(nonatomic, assign) BOOL headingPaused;
@property(nonatomic, copy) RequestLocationAuthHandle requestLocationAuthHandle;

- (void)addDelegate:(id<LcLocationCenterDelegate>)delegate;
- (void)removeDelegate:(id<LcLocationCenterDelegate>)delegate;

- (void)startUpdating;
- (void)stopUpdating;

- (void)startUpdatingLocation;
- (void)stopUpdatingLocation;

- (void)startUpdatingHeading;
- (void)stopUpdatingHeading;

- (void)updateGpsAccuracyMode;

@end

@implementation LcLocationCenter

- (BOOL)authorizationNotDetermined {
  return [CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined;
}

- (BOOL)authorized {
  CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
  return (status == kCLAuthorizationStatusAuthorizedAlways ||
          status == kCLAuthorizationStatusAuthorizedWhenInUse);
}

- (void)requestAuthorization:(RequestLocationAuthHandle)requestHandle {
  if ([self authorizationNotDetermined]) {
    self.requestLocationAuthHandle = requestHandle;
    [self.locationManager requestWhenInUseAuthorization];
  } else {
    if (requestHandle != nil) {
      requestHandle([self authorized]);
    }
  }
}

- (BOOL)locationEnabled {
  return [CLLocationManager locationServicesEnabled] && [self authorized];
}

- (void)internalInit {
  LcLog(@"locationManager.enabled=%d",
        [CLLocationManager locationServicesEnabled]);
  self.locationManager = [[CLLocationManager alloc] init];
  self.locationManager.delegate = self;
  [self enableHighAccuracyGpsMode:NO];
  self.weakDelegates = [NSMutableArray array];
}

- (void)startUpdating {
  [self startUpdatingLocation];
  [self startUpdatingHeading];
}

- (void)stopUpdating {
  [self stopUpdatingLocation];
  [self stopUpdatingHeading];
}

- (void)updateGpsAccuracyMode {
  BOOL clientsUsingHighAccuracyMode = [self clinetsUsingHighAccuracyMode];
  if (!self.usingHighAccuracyMode && clientsUsingHighAccuracyMode) {
    [self enableHighAccuracyGpsMode:YES];
  } else if (self.usingHighAccuracyMode && !clientsUsingHighAccuracyMode) {
    [self enableHighAccuracyGpsMode:NO];
  }
}

- (BOOL)clientsUpdatingLocation {
  NSArray* weakDelegates = [NSArray arrayWithArray:self.weakDelegates];
  for (LcWeakReference* weakDelegate in weakDelegates) {
    id<LcLocationCenterDelegate> delegate = weakDelegate.object;
    if (delegate != nil && delegate.updatingLocation) {
      return YES;
    }
  }
  return NO;
}

- (BOOL)clientsUpdatingHeading {
  NSArray* weakDelegates = [NSArray arrayWithArray:self.weakDelegates];
  for (LcWeakReference* weakDelegate in weakDelegates) {
    id<LcLocationCenterDelegate> delegate = weakDelegate.object;
    if (delegate != nil && delegate.updatingHeading) {
      return YES;
    }
  }
  return NO;
}

- (BOOL)clinetsUsingHighAccuracyMode {
  NSArray* weakDelegates = [NSArray arrayWithArray:self.weakDelegates];
  for (LcWeakReference* weakDelegate in weakDelegates) {
    id<LcLocationCenterDelegate> delegate = weakDelegate.object;
    if (delegate != nil && delegate.useHighAccuracyMode && delegate.updatingLocation) {
      return YES;
    }
  }
  return NO;
}

- (void)startUpdatingLocation {
  LC_CHECK_IS_MAIN_THREAD();
  [self updateGpsAccuracyMode];

  if (!self.locationStarting && [self clientsUpdatingLocation]) {
    self.locationStarting = YES;
    [self doManagerStartUpdatingLocation];
  }
}

- (void)stopUpdatingLocation {
  LC_CHECK_IS_MAIN_THREAD();
  [self updateGpsAccuracyMode];
  
  if (self.locationStarting && ![self clientsUpdatingLocation]) {
    self.locationStarting = NO;
    [self doManagerStopUpdatingLocation];
  }
}

- (CLLocation*)lastKnownLocation {
  return self.locationManager.location;
}

- (void)startUpdatingHeading {
  LC_CHECK_IS_MAIN_THREAD();
  if (!CLLocationManager.headingAvailable) {
    return;
  }
  
  if (!self.headingStarting && [self clientsUpdatingHeading]) {
    self.headingStarting = YES;
    [self doManagerStartUpdatingHeading];
  }
}

- (void)stopUpdatingHeading {
  LC_CHECK_IS_MAIN_THREAD();
  if (self.headingStarting && ![self clientsUpdatingHeading]) {
    self.headingStarting = NO;
    [self doManagerStopUpdatingHeading];
  }
}

- (CLHeading*)lastKnownHeading {
  return self.locationManager.heading;
}

- (void)addDelegate:(id<LcLocationCenterDelegate>)delegate {
  LC_CHECK_NOT_NULL(delegate);
  BOOL found = NO;
  for (int i = ((int)self.weakDelegates.count) - 1; i >= 0; --i) {
    LcWeakReference* weakDelegate = self.weakDelegates[i];
    if (weakDelegate.object == nil) {
      [self.weakDelegates removeObjectAtIndex:i];
    } else if (weakDelegate.object == delegate) {
      found = YES;
    }
  }
  if (!found) {
    [self.weakDelegates
        addObject:[LcWeakReference weakReferenceWithObject:delegate]];
  }
}

- (void)removeDelegate:(id<LcLocationCenterDelegate>)delegate {
  LC_CHECK_NOT_NULL(delegate);
  for (int i = ((int)self.weakDelegates.count) - 1; i >= 0; --i) {
    LcWeakReference* weakDelegate = self.weakDelegates[i];
    if (weakDelegate.object == nil || weakDelegate.object == delegate) {
      [self.weakDelegates removeObjectAtIndex:i];
      break;
    }
  }
}

#pragma mark - delegate

- (void)locationManager:(CLLocationManager*)manager didUpdateLocations:(NSArray*)locations {
  static int ROUGH_ACCURACY_COUNTER = 0;
#ifdef DEBUG
  LC_CHECK_IS_MAIN_THREAD();
#endif
  LcLog(@"locationManager:didUpdateLocations:");
  CLLocation* location = [locations lastObject];
  double accuracy = location.horizontalAccuracy;
  if (self.lastKnownLocation == nil ||
      location.horizontalAccuracy <= self.lastKnownLocation.horizontalAccuracy / 3) {
    // first point
    // or accuracy improves a lot
    // do nothing & continue
  } else if ((accuracy > 500 && ROUGH_ACCURACY_COUNTER < 3) ||
      (accuracy > 200 && accuracy <= 500 && ROUGH_ACCURACY_COUNTER < 2) ||
      (accuracy > 50 && accuracy <= 200 && ROUGH_ACCURACY_COUNTER < 1)) {
    // drop rough accuracy location
    ROUGH_ACCURACY_COUNTER++;
    return;
  }
  ROUGH_ACCURACY_COUNTER = 0;
  
  for (int i = ((int)self.weakDelegates.count) - 1; i >= 0; --i) {
    LcWeakReference* weakDelegate = self.weakDelegates[i];
    id<LcLocationCenterDelegate> delegate = weakDelegate.object;
    if (delegate == nil) {
      [self.weakDelegates removeObjectAtIndex:i];
    } else {
      [delegate onLocationChanged:location];
    }
  }
}

- (void)locationManager:(CLLocationManager*)manager didUpdateHeading:(CLHeading*)newHeading {
#ifdef DEBUG
  LC_CHECK_IS_MAIN_THREAD();
#endif
//  LcLog(@"locationManager:didUpdateHeading:");
  for (int i = ((int)self.weakDelegates.count) - 1; i >= 0; --i) {
    LcWeakReference* weakDelegate = self.weakDelegates[i];
    id<LcLocationCenterDelegate> delegate = weakDelegate.object;
    if (delegate == nil) {
      [self.weakDelegates removeObjectAtIndex:i];
    } else {
      if ([delegate respondsToSelector:@selector(onHeadingChanged:)]) {
        [delegate onHeadingChanged:newHeading];
      }
    }
  }
}

- (void)locationManager:(CLLocationManager*)manager
       didFailWithError:(NSError*)error {
  LcLog(@"locationManager:didFailWithError: error=%@", error);
}

- (void)locationManager:(CLLocationManager*)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
  LcLog(@"locationManager:didChangeAuthorizationStatus: status=%d", status);
  BOOL accessGranted = (status == kCLAuthorizationStatusAuthorizedAlways ||
                        status == kCLAuthorizationStatusAuthorizedWhenInUse);
  if (self.requestLocationAuthHandle != nil) {
    self.requestLocationAuthHandle(accessGranted);
    self.requestLocationAuthHandle = nil;
  }
  for (int i = ((int)self.weakDelegates.count) - 1; i >= 0; --i) {
    LcWeakReference* weakDelegate = self.weakDelegates[i];
    id<LcLocationCenterDelegate> delegate = weakDelegate.object;
    if (delegate == nil) {
      [self.weakDelegates removeObjectAtIndex:i];
    } else {
      if ([delegate
              respondsToSelector:@selector(onLocationAuthorizedChanged:)]) {
        [delegate onLocationAuthorizedChanged:self.authorized];
      }
    }
  }
//  if (accessGranted) {
//    if (self.locationStarting && !self.locationPaused) {
//      [self doManagerStopUpdatingLocation];
//    }
//  } else {
//    [self doManagerStopUpdatingLocation];
//  }
}

#pragma mark - Manager

- (void)pauseIfStarted {
  if (self.locationStarting) {
    [self doManagerStopUpdatingLocation];
    self.locationPaused = YES;
  }
  if (self.headingStarting) {
    [self doManagerStopUpdatingHeading];
    self.headingPaused = YES;
  }
}

- (void)resumeIfPaused {
  if (self.locationPaused) {
    [self doManagerStartUpdatingLocation];
    self.locationPaused = NO;
  }
  if (self.headingPaused) {
    [self doManagerStartUpdatingHeading];
    self.headingPaused = NO;
  }
}

- (void)enableHighAccuracyGpsMode:(BOOL)enabled {
  if (enabled) {
    self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
    self.locationManager.distanceFilter = 0;
  } else {
    self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
    self.locationManager.distanceFilter = 500;
  }

  if (self.usingHighAccuracyMode != enabled) {
    self.usingHighAccuracyMode = enabled;
    if (self.locationStarting) {
      [self doManagerStopUpdatingLocation];
      [self doManagerStartUpdatingLocation];
    }
  }
}

- (void)doManagerStartUpdatingLocation {
  // reset delegate to avoid system bug
  self.locationManager.delegate = self;
  [self.locationManager startUpdatingLocation];
}

- (void)doManagerStopUpdatingLocation {
  [self.locationManager stopUpdatingLocation];
}

- (void)doManagerStartUpdatingHeading {
  [self.locationManager startUpdatingHeading];
}

- (void)doManagerStopUpdatingHeading {
  [self.locationManager stopUpdatingHeading];
}

@end
